游戏开发中的 MVC 模式
注意:这里只是谈谈我理解的 MVC 模式(游戏),而实际后端的 MVC 模式和这个不一样
MVC 的构成
MVC 是三个单词的缩写,分别为:模型(Model)、视图(View)和控制(Controller)。
模型是数据层,视图是表现层,控制器是逻辑层,也对应于程序运行中的数据输入,数据处理,数据输出基本三步骤。
注意:MVC 这个模式是由三个基本的设计模式组成的
- 策略模式
- 观察者模式
- 组合模式
如上图所示,策略模式表示 MVC 中的 Controller 部分。策略模式将用户输入与游戏的逻辑(Model)和接口(View)分离。
组合设计模式表示应用程序中的所有视图(主窗口和按钮)。此设计模式为模型的所有视图提供统一的访问点。
观察者模式代表你的应用程序(模型)中的逻辑。通过这种模式,模型能够与视图和控制器进行交互,而不需要知道它们内部细节。这种模式使所有类之间的交互有更低的耦合度。
注意:一般所有通信都是单向的。
View 传送指令到 Controller
Controller 完成业务逻辑后,要求 Model 改变状态
Model 将新的数据发送到 View,用户得到反馈
互动模式的两种方式
接受用户指令时,MVC 可以分成两种方式。一种是通过 View 接受指令,传递给 Controller。
另一种是直接通过 Controller 接受指令。
数据层
数据层就是各种资源(图片,声音,动画)在游戏引擎中形成的对象集合。
美术提供的这些是最原始的,需要游戏引擎封装成一组可控的代码。
在 Cocos2D 中就用相应的对象来直接初始化这些原始资源,比如 Sprit,Menu,SimpleAudioEngine 等等。这些对象在整个游戏中可以划分为数据层。
最好的例子就是 CocosBuilder 生成的 ccb 文件通过引擎解析而成的类。当最原始的资源文件通过一定的文件格式组织起来,进而通过引擎形成对象集合,这是最可取的数据层生成方法。
比如多国语言的本地化。所有的字段的翻译通过一个 plist 文件存储,形成一张二维表。当需要切换语言时,只需要改变某个 key 相对的 Value 就行了。逻辑层不需要做任何改变。
数据层中的观察者模式
观察者模式是实现 MVC 的关键模式。或者说,MVC 模式就是基于观察者模式的。
在 MVC 模式中有3个参与者,其各自职责如下:
视图(View):负责显示数据(只负责显示数据,不做其他操作);负责通知用户在视图界面上的操作(给控制器或其他参与者)。
控制器(Controller):负责事件处理;负责显示逻辑/业务逻辑。
模型(Model or ViewModel):
- 负责获取数据(视图的);
- 负责维持数据状态;
- 负责通知数据变更(给视图或控制器)。
可以看到,在MVC各参与者都有自己,他们之间用消息机制传递变化。
监听函数的悬垂引用
监听者模式实现了各参与者的解耦,通过消息来实现相互协作。发布者不知道订阅者的具体功能与运行状态。订阅者知道发布者存在,以及会在某个不特定的时刻调用它注册的监听函数;除此之外,它对订阅者也一无所知。订阅者的实体对象与发布者实体对象的生命周期也完全无关,可能订阅者实体对象先被销毁,也可能发布者先于订阅者被销毁。
通常订阅者会在内部维护一个订阅者的监听者列表。当订阅者先于发布者被销毁,订阅者注册到发布者中去的监听函数是无效函数。
可以让订阅者采用 Dispose 模式来避免监听函数的悬垂引用:
订阅者提供一个 dispose 函数,订阅者显示调用 dispose 来结束生命周期。在 dispose 函数中,订阅者退订消息。 采取某种垃圾回收机制,当检测到订阅者不再有效时,垃圾回收代码调用订阅者的 dispose 函数来退订消息。
例如:
public void OnDestroy() {
EventManager.Remove(this);
}
逻辑层:策略模式
逻辑层可以划分为:
- 数据接口层
- 游戏 AI 层
- 操作控制层。
数据接口层 是负责从数据层中提供相应的数据对象进行封装组合。在这个层次上,类似与面向对象中类的属性定义,并提供控制(Set/Get)接口。这一层既可以是无结构化(原始数据类型定义),也可以是结构化(表,树,集合)。
游戏 AI 层 是逻辑层之核心,定义了其下面的各种数据元素的状态变化(金币数量,武器等级,动画播放……)。根据不同的状态变量,控制着数据元素的状态表现,是游戏的大脑,指挥控制中心。
操作控制层,负责处理用户的输入,并注册或绑定相应的 AI 事件。
这里拿操作控制层举例:
在游戏中,你应该将输入控制器和游戏逻辑之间的交互进行解耦。游戏的逻辑应该接收相同类型的输入,而不管输入控制器是什么(按钮,手势,操纵杆)。
尽管对用户每个输入控制器的行为表现不同,但它们必须向游戏的逻辑提供相同的数据。此外,添加或删除输入控制器不应导致游戏崩溃。
这种解耦行为和灵活性是可能的,这归功于策略设计模式。这种设计模式允许通过动态方式来改变行为,而不需要修改游戏的任何逻辑,为你的游戏提供了很高的灵活性。
总之:在逻辑层中,数据接口层向下面对数据层,操作控制层向上面对用户行为事件,而游戏AI层统筹这两层。
视图层:组合模式
游戏通常包含许多视图。主视图中显示角色。有一个子视图,显示玩家的积分。有一个子视图,显示游戏中剩下的时间。如果你在移动设备上玩游戏,那么每个按钮都是一个视图。
可维护性应该是游戏开发过程中的主要关注点。每个视图不应具有不同的函数名称或不同的访问点。相反,你想要为每个视图提供一个统一的访问点,即相同的函数调用应该既能够访问主视图也能够访问子视图。
这种统一的接入点可以使用复合设计模式。此模式将每个视图放置在树状结构中,从而为每个视图提供统一的访问点。取代了需要用不同的函数来访问不同的子视图,组合模式可以用相同的函数访问任何视图。
视图层的职责
View 负责与用户的交互,交互又分为两种:输入和输出。
输入:输入就是收集玩家的操作,例如玩家点击了一个按钮,或者输入了某些文字。例如,当玩家点击了一个好友的头像,或者点击了删除好友。就需要对这些输入进行响应,对数据做出相应的处理。
输出:输出就是将游戏中各种数据,展示出来(包括视图展示,特效,声音等),让玩家能够看清楚,看明白。例如,上述中的 FriendModel 中保存的数据,输出就负责将其显示出来,让玩家看到这个好友的性别是男或者女,而不是1或者0,或是其它什么的。
补充:MVP 模式
MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。
- 各部分之间的通信,都是双向的。
- View 与 Model 不发生联系,都通过 Presenter 传递。
- View 非常薄,不部署任何业务逻辑,称为 "被动视图"(Passive View),即没有任何主动性,而 Presenter 非常厚,所有逻辑都部署在那里。
Presenter 负责和 Model 进行双向交互,还有和 View 进行双向交互所以如果业务复杂一点,Presenter的体积增大、臃肿,就很难维护;
补充:MVVM 模式
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。即:Model-View-ViewModel
唯一的区别是,它采用双向绑定(data-binding):View 的变动,自动反映在 ViewModel,反之亦然。
Angular 和 Ember 都采用这种模式。
ViewModel,主要是胶水层,核心思想是简化,它做两件事:
- 数据发生变化,如何知道变化,通过数据响应式的机制,用某种机制知道这个数据的变化,自动的去响应数据的变化,自动去做更新;内部知道了数据的变化,不需要用户来操作。
- 更新:以前是直接用 JQ 进行 Dom 操作,每次数据变了都要自己操作 Dom 来更新(代码多,效率不高);所以有了虚拟 Dom 的方式去做更新,根据精准的 diff 算法来做比对;达到高效的结果。
补充:前后端 MVC 区别
后端 MVC 中的 view 是前端 MVC 的全部,一般来说,前端MVC大部分都是MVVM(MVC的强类型转换高级版),包括 angularJS(当然后端中也有 MVVM 的,比如 WPF)。